using System;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Navigation;

namespace Ebt.Mvvm
{
    /// <summary>
    /// Encapsulates the <see cref="Frame"/> navigation functionality.
    /// You should add a property of this type to your "root" view model and bind it to your <see cref="Frame"/> using <see cref="ElementBinder.WrapperProperty"/>.
    /// </summary>
    public class NavigationState : IElementWrapper
    {
        private Frame _frame;
        private NavigationArgs _navigationArgs;

        #region Private properties

        private Page Page { get { return _frame.Content as Page; } }

        private IPageModel PageModel
        {
            get { return Page == null ? null : Page.DataContext as IPageModel; }
        }

        #endregion

        #region Public members

        /// <summary>
        /// Navigates to the specified page.
        /// </summary>
        /// <param name="pageType">The page view type.</param>
        public void Navigate(Type pageType)
        {
            if (pageType == null)
                throw new ArgumentNullException("pageType");

            if (_frame == null)
                return;

            Navigate(NavigationMode.New, () => _frame.Navigate(pageType));
        }

        /// <summary>
        /// Navigates to the specified page.
        /// </summary>
        /// <param name="pageType">The page view type.</param>
        /// <param name="parameter">A user defined parameter that will be saved in the navigation history stack and will be passed to the page.</param>
        public void Navigate(Type pageType, object parameter)
        {
            if (pageType == null)
                throw new ArgumentNullException("pageType");

            if (_frame == null)
                return;

            Navigate(NavigationMode.New, () => _frame.Navigate(pageType, parameter));
        }

        /// <summary>
        /// Navigates backward.
        /// </summary>
        public void GoBack()
        {
            if (_frame == null || !_frame.CanGoBack)
                return;

            Navigate(NavigationMode.Back, () => _frame.GoBack());
        }

        /// <summary>
        /// Navigates forward.
        /// </summary>
        public void GoForward()
        {
            if (_frame == null || !_frame.CanGoForward)
                return;

            Navigate(NavigationMode.Forward, () => _frame.GoForward());
        }

        /// <summary>
        /// Allows to restore the navigation history.
        /// </summary>
        /// <param name="state">A value returned by <see cref="GetNavigationState"/> method.</param>
        public void SetNavigationState(string state)
        {
            if (PageModel != null)
                PageModel.OnNavigatedFrom(new NavigationArgs { NavigationMode = NavigationMode.Forward });

            _frame.SetNavigationState(state);

            try
            {
                var naviagtionStack = NavigationStackParser.Parse(state);
                _navigationArgs = new NavigationArgs
                {
                    Parameter = naviagtionStack.Items[naviagtionStack.CurrentIndex].Parameter,
                    NavigationMode = NavigationMode.Back
                };
            }
            catch (Exception)
            {
                _navigationArgs = null;
            }

            NotifyModelOnNavigatedTo(_navigationArgs);
        }

        /// <summary>
        /// Returns the navigation history.
        /// </summary>
        /// <returns>The serialized navigation history.</returns>
        public string GetNavigationState()
        {
            var state = _frame.GetNavigationState();
            if (PageModel != null)
            {
                PageModel.OnNavigatedFrom(new NavigationArgs
                {
                    NavigationMode = NavigationMode.Forward
                });
            }
            return state;
        }

        /// <summary>
        /// Clears the navigation history and navigates out of any page.
        /// </summary>
        public void Reset()
        {
            if (_frame == null)
                return;

            var initialState = new Frame().GetNavigationState();

            // clear cache
            var previousCacheSize = _frame.CacheSize;
            _frame.CacheSize = 0;
            _frame.CacheSize = previousCacheSize;

            if (PageModel != null)
                PageModel.OnNavigatedFrom(new NavigationArgs { NavigationMode = NavigationMode.New });

            _frame.SetNavigationState(initialState);
        }

        /// <summary>
        /// The current page view type.
        /// </summary>
        public Type CurrentPageType
        {
            get { return _frame.SourcePageType; }
        }

        /// <summary>
        /// Gets the value indicating whether navigation history contains a previous page or not.
        /// </summary>
        public bool CanGoBack
        {
            get { return _frame != null && _frame.CanGoBack; }
        }

        /// <summary>
        /// Gets the value indicating whether navigation history contains a next page or not.
        /// </summary>
        public bool CanGoForward
        {
            get { return _frame != null && _frame.CanGoForward; }
        }

        /// <summary>
        /// Occurs when a navigation to a page is done.
        /// </summary>
        public event NavigatedEventHandler Navigated
        {
            add { _frame.Navigated += value; }
            remove { _frame.Navigated -= value; }
        }

        /// <summary>
        /// Occurs when a navigation from a page is requested.
        /// Allows to cancel the navigation.
        /// </summary>
        public event NavigatingCancelEventHandler Navigating
        {
            add { _frame.Navigating += value; }
            remove { _frame.Navigating -= value; }
        }

        #endregion

        #region Private methods

        private void OnFrameNavigating(object sender, NavigatingCancelEventArgs e)
        {
            if (PageModel == null)
                return;

            bool cancel = e.Cancel;

            PageModel.OnNavigatingFrom(ref cancel);
            PageModel.IsPageActive = cancel;

            e.Cancel = cancel;
        }

        private void OnFrameNavigated(object sender, NavigationEventArgs e)
        {
            _navigationArgs = new NavigationArgs
            {
                NavigationMode = e.NavigationMode,
                Parameter = e.Parameter
            };
            NotifyModelOnNavigatedTo(_navigationArgs);
        }

        private void OnPageLoaded(object sender, RoutedEventArgs e)
        {
            Page.Loaded -= OnPageLoaded;

            TryNotifyModelOnNavigatedTo(_navigationArgs);
        }

        private void NotifyModelOnNavigatedTo(NavigationArgs args)
        {
            if (!TryNotifyModelOnNavigatedTo(args))
            {
                // We was not able to notify the model. Try again later.
                Page.Loaded -= OnPageLoaded;
                Page.Loaded += OnPageLoaded;
            }
        }

        private bool TryNotifyModelOnNavigatedTo(NavigationArgs args)
        {
            if (PageModel == null)
                return false;

            PageModel.IsPageActive = true;

            PageModel.OnNavigatedTo(args);

            return true;
        }

        private void Navigate(NavigationMode mode, Action navigate)
        {
            if (_frame == null)
                return;

            var page1Type = _frame.CurrentSourcePageType;
            var page1Model = PageModel;

            navigate();

            var page2Type = _frame.SourcePageType;

            bool isPageChanged = page1Type != page2Type;
            if (isPageChanged && page1Model != null)
            {
                var args = new NavigationArgs { NavigationMode = mode, Parameter = _navigationArgs.Parameter };
                page1Model.OnNavigatedFrom(args);
            }
        }

        #endregion

        #region Implementation of IElementWrapper

        bool IElementWrapper.HasElement(object d)
        {
            return ReferenceEquals(d, _frame);
        }

        void IElementWrapper.AddElement(object d)
        {
            if (_frame != null)
                throw new NotSupportedException("Multiple navigation Frames are not supported.");

            _frame = (Frame)d;
            _frame.Navigating += OnFrameNavigating;
            _frame.Navigated += OnFrameNavigated;
        }

        void IElementWrapper.RemoveElement(object d)
        {
            if (!ReferenceEquals(d, _frame))
                throw new ArgumentException("The navigation Frame could not be removed.", "d");

            _frame.Navigating -= OnFrameNavigating;
            _frame.Navigated -= OnFrameNavigated;
            _frame = null;
        }

        #endregion
    }
}